/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package com.up.vms.infrastructure.security;


import com.up.vms.infrastructure.exception.UbpFrameException;
import com.up.vms.infrastructure.mvc.dto.HttpHeaders;
import com.up.vms.infrastructure.util.StringUtilsExt;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLDecoder;
import java.security.*;
import java.util.*;
import java.util.Map.Entry;

/**
 * 后端服务签名示例
 */
@Slf4j
public class Sign {
    //API网关中所有参与签名计算的HTTP请求头的Key,以应为逗号分割
    private static final String CA_PROXY_SIGN_HEADERS = StringUtils.join(new String[]{
            HttpHeaders.X_EDI_STATGE,
            HttpHeaders.X_EDI_CLIENT_KEY,
            HttpHeaders.X_EDI_TIME,
            HttpHeaders.X_EDI_USER_ID,
            HttpHeaders.X_EDI_REQUEST_ID,
    }, ",");
    //签名算法HmacSha256
    public static final String HMAC_SHA256 = "HmacSHA256";
    //换行符
    private static char lineFeed = '\n';
    //编码
    private static final String ENCODING = "UTF-8";
    //HTTP POST
    private static final String HTTP_METHOD_POST = "POST";
    //HTTP PUT
    private static final String HTTP_METHOD_PUT = "PUT";
    
    private Sign() {
    	
    }
    
    public static String serviceSign(HttpServletRequest request, String secret) throws Exception {
        try {
            String uri = URLDecoder.decode(request.getRequestURI(), "UTF-8"); 
            String httpMethod = request.getMethod().toUpperCase();
            Map<String, String> headers = new HashMap<>();
            Enumeration<String> headerNames = request.getHeaderNames();
            while (headerNames.hasMoreElements()) {
                String headerName = headerNames.nextElement();
                headers.put(headerName, request.getHeader(headerName));
            }
            Map<String, Object> paramMap = new HashMap<>();
            for(Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
                paramMap.put(entry.getKey(), entry.getValue());
            }
            byte[] inputStreamBytes = getRequestInputStream(request);
            return  Sign.serviceSign(uri, httpMethod, secret, headers, paramMap, inputStreamBytes);
        }catch (Exception e) {
            throw new Exception(e);
        }
    }

    public static byte[] getRequestInputStream(HttpServletRequest request) {
        byte[] inputStreamBytes;
        try {
            ServletInputStream inputStream = request.getInputStream();
            if (inputStream != null) {
                inputStreamBytes = IOUtils.toByteArray(inputStream);
            } else {
                inputStreamBytes = new byte[]{};
            }
        } catch (IOException e) {
            inputStreamBytes = new byte[]{};
        }
        return inputStreamBytes;
    }

    /**
     * 计算HTTP请求签名
     *
     * @param uri              原始HTTP请求PATH（不包含Query）
     * @param httpMethod       原始HTTP请求方法
     * @param secret           应用的secret
     * @param headers          原始HTTP请求所有请求头
     * @param paramsMap        原始HTTP请求所有Query+Form参数
     * @param inputStreamBytes 原始HTTP请求Body体（仅当请求为POST/PUT且非表单请求才需要设置此属性,表单形式的需要将参数放到paramsMap中）
     * @return 签名结果
     * @throws Exception
     */
    public static String serviceSign(String uri, String httpMethod, String secret, Map<String, String> headers, Map<String, Object> paramsMap, byte[] inputStreamBytes) throws UbpFrameException {
        String httpMethodUpper = httpMethod.toUpperCase();
        Map<String, String> headersToSign = buildHeadersToSign(headers);
        try {
            String bodyMd5 = buildReqBodyToMd5(httpMethodUpper, inputStreamBytes);
            log.info("Server bodyMD5: "+bodyMd5);

            String resourceToSign = buildResource(uri, paramsMap);
            log.info("Server resourceToSign: "+resourceToSign);
            String stringToSign = buildStringToSign(headersToSign, resourceToSign, httpMethodUpper, bodyMd5);
            log.info("Server stringToSign: "+stringToSign);

            Mac hmacSha256 = Mac.getInstance(HMAC_SHA256);

            byte[] keyBytes = secret.getBytes(ENCODING);
            hmacSha256.init(new SecretKeySpec(keyBytes, 0, keyBytes.length, HMAC_SHA256));

            return new String(Base64.encodeBase64(hmacSha256.doFinal(stringToSign.getBytes(ENCODING))), ENCODING);
        } catch (Exception e) {
            throw new UbpFrameException(e);
        }
    }

    /**
     * 构建BodyMd5
     *
     * @param httpMethod       HTTP请求方法
     * @param inputStreamBytes HTTP请求Body体字节数组
     * @return Body Md5值
     */
    private static String buildReqBodyToMd5(String httpMethod, byte[] inputStreamBytes) throws IOException {
        if (inputStreamBytes == null) {
            return null;
        }

        if (!httpMethod.equalsIgnoreCase(HTTP_METHOD_POST) && !httpMethod.equalsIgnoreCase(HTTP_METHOD_PUT)) {
            return null;
        }

        InputStream inputStream = new ByteArrayInputStream(inputStreamBytes);
        byte[] bodyBytes = IOUtils.toByteArray(inputStream);
        if (bodyBytes != null && bodyBytes.length > 0) {
            return base64AndMD5(bodyBytes).trim();
        }
        return null;
    }

    /**
     * 将Map转换为用&及=拼接的字符串
     */
    private static String buildReqMapToSign(Map<String, Object> paramMap) {
        StringBuilder builder = new StringBuilder();

        for (Entry<String, Object> e : paramMap.entrySet()) {
            if (builder.length() > 0) {
                builder.append('&');
            }
            handleMap(builder, e);
        }

        return builder.toString();
    }

    private static void handleMap(StringBuilder builder, Entry<String, Object> e) {
        if (e.getValue() != null && e.getValue() instanceof List) {
            List list = (List) e.getValue();
            if (CollectionUtils.isEmpty(list)) {
                builder.append(e.getKey());
            } else {
                builder.append(e.getKey()).append("=").append(String.valueOf(list.get(0)));
            }
        } else if (e.getValue() != null && e.getValue() instanceof Object[]) {
            Object[] objs = (Object[]) e.getValue();
            if (objs.length == 0) {
                builder.append(e.getKey());
            } else {
                builder.append(e.getKey()).append("=").append(String.valueOf(objs[0]));
            }
        } else if (e.getValue() != null) {
            builder.append(e.getKey()).append("=").append(String.valueOf(e.getValue()));
        }
    }

    /**
     * 构建参与签名的HTTP头
     * <pre>
     * 传入的Headers必须将默认的ISO-8859-1转换为UTF-8以支持中文
     * </pre>
     *
     * @param headers HTTP请求头
     * @return 所有参与签名计算的HTTP请求头
     */
    private static Map<String, String> buildHeadersToSign(Map<String, String> headers) {
    	 Map<String, String> lowerHeaders = new HashMap<>();

         //此处header统一转换成小写，防止客户端自动将headers装换小写导致取值失败
         for (Entry<String,String> entry : headers.entrySet()) {
             lowerHeaders.put(entry.getKey().toLowerCase(), entry.getValue());
         }

         Map<String, String> headersToSignMap = new LinkedHashMap<>();
         if (CA_PROXY_SIGN_HEADERS != null) {
             for (String headerKey : CA_PROXY_SIGN_HEADERS.split(",")) {
                 headersToSignMap.put(headerKey, lowerHeaders.get(headerKey.toLowerCase()));
             }
         }

         return headersToSignMap;
    }

    /**
     * 组织待计算签名字符串
     *
     * @param headers        HTTP请求头
     * @param resourceToSign Uri+请求参数的签名字符串
     * @param method         HTTP方法
     * @param bodyMd5        Body Md5值
     * @return 待计算签名字符串
     */
    private static String buildStringToSign(Map<String, String> headers, String resourceToSign, String method, String bodyMd5) {
        StringBuilder sb = new StringBuilder();
        sb.append(method.toUpperCase()).append(lineFeed);
        sb.append(buildHeaders(headers));
        sb.append(resourceToSign).append(lineFeed);
        if (StringUtils.isNotBlank(bodyMd5)) {
            sb.append(bodyMd5);
        }
        return sb.toString();
    }

    /**
     * 组织Headers签名签名字符串
     *
     * @param headers HTTP请求头
     * @return Headers签名签名字符串
     */
    private static String buildHeaders(Map<String, String> headers) {
        StringBuilder sb = new StringBuilder();
        for (Entry<String, String> e : headers.entrySet()) {
            if (e.getValue() != null) {
                sb.append(e.getKey()).append(':').append(e.getValue()).append(lineFeed);
            }
        }
        return sb.toString();
    }

    /**
     * 组织Uri+请求参数的签名字符串
     *
     * @param uri       HTTP请求uri,不包含Query
     * @param paramsMap HTTP请求所有参数（Query+Form参数）
     * @return Uri+请求参数的签名字符串
     */
    private static String buildResource(String uri, Map<String, Object> paramsMap) {
        StringBuilder builder = new StringBuilder();

        // uri
        builder.append(uri);

        // Query+Form
        TreeMap<String, Object> sortMap = new TreeMap<>();
        sortMap.putAll(paramsMap);


        // 有Query+Form参数
        if (sortMap.size() > 0) {
            builder.append('?');
            builder.append(buildReqMapToSign(sortMap));
        }

        return builder.toString();
    }

    /**
     * 先进行MD5摘要再进行Base64编码获取摘要字符串
     *
     * @param bytes 待计算字节数组
     */
    public static String base64AndMD5(byte[] bytes) {
        if (bytes == null) {
            throw new IllegalArgumentException("bytes can not be null");
        }

        try {
            final MessageDigest md = MessageDigest.getInstance("MD5");
            md.reset();
            md.update(bytes);
            final Base64 base64 = new Base64();

            return new String(base64.encode(md.digest()));
        } catch (final NoSuchAlgorithmException e) {
            throw new IllegalArgumentException("unknown algorithm MD5");
        }
    }


    /**
     * SHA-256加密 返回16进制字符串
     *
     * @param bytes
     * @return
     */
    public static String sha256Encrypt(byte[] bytes) {
        if (null == bytes) {
            throw new IllegalArgumentException("bytes can not be null");
        }
        try {
            final MessageDigest md = MessageDigest.getInstance("SHA-256");
            md.reset();
            md.update(bytes);
            return StringUtilsExt.bytesToHexString(md.digest());
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalArgumentException("unknown algorithm SHA-256");
        }
    }

    /**
     * MD5 加密，返回16进制字符串
     * @param bytes
     * @return
     */
    public static String mdEncrypt(byte[] bytes) {
        if (null == bytes) {
            throw new IllegalArgumentException("bytes can not be null");
        }
        try {
            final MessageDigest md5 = MessageDigest.getInstance("MD5");
            md5.reset();
            md5.update(bytes);
            return StringUtilsExt.bytesToHexString(md5.digest());
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalArgumentException("unknown algorithm MD5");
        }
    }

    /**
     * SHA1 加密，返回16进制字符串
     * @param bytes
     * @return
     */
    public static String sha1Encrypt(byte[] bytes) {
        if (null == bytes) {
            throw new IllegalArgumentException("bytes can not be null");
        }
        try {
            final MessageDigest md = MessageDigest.getInstance("SHA-1");
            md.reset();
            md.update(bytes);
            return StringUtilsExt.bytesToHexString(md.digest());
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalArgumentException("unknown algorithm SHA-1");
        }
    }

    /**
     * 使用SHA1withRSA签名
     */
    public static String sha1WithRSASign(byte[] bytes, PrivateKey privateKey) throws Exception{

        try {
            Signature signature = Signature.getInstance("SHA1withRSA");
            signature.initSign(privateKey);
            signature.update(bytes);

            return Base64.encodeBase64String(signature.sign()).replaceAll(System.getProperty("line.separator"), "");
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalArgumentException("unknown algorithm RSA");
        } catch (InvalidKeyException|SignatureException e) {
            throw new Exception(e);
        }

    }

    public static String sha1WithRSASign(byte[] bytes, String path, String pwd) throws Exception{
        try {
            PrivateKey privateKey = CertUtil.getPrivateKeyInfo(path, pwd);
            return sha1WithRSASign(bytes, privateKey);
        } catch (Exception e) {
            throw new Exception(e);
        }
    }

}
